<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>图文点读</title>
  <script src="../common/cos-js-sdk-v5.min.js"></script>
  <style>
    html,body {
      margin: 0;
      padding: 0;      
    }
    .container {        
      padding: 20px;
    }
    .result-area {
      width: 700px;      
      height: auto;      
    }
  </style>
</head>
<body>
  <div class="container">
    <div>      
      <input type="file" id="file-selector"/>
      <span id="err-msg"></span>
    </div>
    <div class="result-area">
      <p id="ocr-result"></p>
      <p id="task-status"></p>
      <audio src="" id="video-result" controls="controls" style="display: none;"></audio>
    </div>
  </div>
  <script src="../common/xml2json.js"></script>
  <script>

    /*
      准备工作：
      1. 在cos控制台创建一个存储桶，可拿到存储桶名称和地域，即Bucket，Region。
      2. 进入存储桶配置页面，设置对应的跨域规则（简单调试可将来源Origin设置为*）。
      3. 进入访问管理控制台，获取API密钥，即SecretId，SecretKey。
    
    /* 图文点读步骤：
      1. 拍照并上传到COS存储桶中
      2. 使用 OCR 功能提取图片中的文字
      3. 创建语音合成任务，产出中/英文音频
    */
            
    // 注意：以下初始化方式仅供联调测试使用，为了安全起见，请勿在生产环境直接暴露密钥。
    // 生产环境请参考各语言SDK签名实现。https://cloud.tencent.com/document/product/436/7778#sdk-.E7.AD.BE.E5.90.8D.E5.AE.9E.E7.8E.B0

    // 密钥请在访问管理控制台获取。https://console.cloud.tencent.com/cam/capi
    let cos = new COS({
      SecretId: 'AKI*****************************',
      SecretKey: '*************************'
    });

    // 存储桶配置请在cos控制台获取。https://console.cloud.tencent.com/cos/bucket
    // 格式参考：Bucket: 'abc-1250000000', Region: 'ap-shanghai'
    let bucketConf = {
      Bucket: 'test-1250000000',
      Region: 'ap-guangzhou'
    };

    let TaskInterval = null;

    const OCRResultsEle = document.getElementById('ocr-result'),
        TaskStatusEle = document.getElementById('task-status'),
        VideoResultEle = document.getElementById('video-result');

    TaskStatusEle.innerHTML = "";
    VideoResultEle.style.display = 'none'
    document.getElementById('file-selector').onchange = function(e) {
        let file = this.files[0];    
        if (!file) {
            return alert('未发现上传文件');
        }
        if(!['image/jpeg', 'image/jpg', 'image/png', 'image/bmp', 'application/pdf'].includes(file.type)) {        
            return alert('当前仅支持图片格式：JPG、JPEG、PNG、BMP 及 PDF 文件');        
        }

        // 1. 上传图片到cos存储桶，接口可参考 https://cloud.tencent.com/document/product/436/7749
        // 注意：为了避免上传出现跨域错误，需要设置跨域访问规则，简单调试可将来源Origin设置为*，参考 https://cloud.tencent.com/document/product/436/13318
        uploadFile('test.png', file, (uploadErr) => {  
            if(uploadErr) {
                console.log(JSON.stringify(uploadErr));
                return alert('上传对象失败');
            }

            // 2. 使用 OCR 功能提取图片中的文字，接口可参考 https://cloud.tencent.com/document/product/460/63227
            cos.request({
                ...bucketConf,
                Key: 'test.png',    // 待识别的图片
                Query: {
                    'ci-process': 'OCR',  
                }
            }, (ocrErr, ocrData) => {
                if (ocrErr) {
                    console.log(JSON.stringify(ocrErr));
                    return alert('识别图片失败');
                }
                const list = ocrData?.Response?.TextDetections?.map(text => text.DetectedText || "");
                const ocrResult = list?.join('\n') || "";
                OCRResultsEle.innerHTML = '识别的内容：\n' + ocrResult;

                // 3. 将识别的内容上传到COS中（ 如果 ocrResult 长度不超过300，可以不用上传到COS，可以直接创建语音合成任务 ）
                uploadFile('content.txt', ocrResult, (OcrUploadErr, data) => {
                    if (OcrUploadErr) {
                        console.log(JSON.stringify(OcrUploadErr));
                        return alert('识别结果上传失败');
                    }
                    TaskStatusEle.innerHTML = "音频生成中...";

                    // 4. 创建语音合成任务
                    const txtUrl = `https://${bucketConf.Bucket}.cos.${bucketConf.Region}.myqcloud.com/content.txt`;

                    createTtsTask(txtUrl, 'result.mp3', (ttsErr, ttsData) => {
                        TaskStatusEle.innerHTML = "";
                        if (ttsErr) {
                            return ttsErr?.code === 'AsrBucketUnBinded' ? alert('使用语音合成需要先开启服务开关') : alert('合成语音失败 ' + ttsErr?.code);
                        }
                        VideoResultEle.setAttribute('src', `https://${bucketConf.Bucket}.cos.${bucketConf.Region}.myqcloud.com/result.mp3`);
                        VideoResultEle.style.display = 'block'
                    })
                })
            })

        });

    }

    /**
     * 上传文件到cos存储桶，参考接口： https://cloud.tencent.com/document/product/436/7749
     * 注意：为了避免上传出现跨域错误，需要设置跨域访问规则，简单调试可将来源Origin设置为*，参考 https://cloud.tencent.com/document/product/436/13318
     **/
    function uploadFile(key, file, callback) {
      cos.putObject({
            ...bucketConf,
            Key: key,    // 目标文件名
            Body: file,
        }, (err, data) => {  
          callback && callback(err, data);
        })
    }
   
    /**
     * 创建语音合成任务，参考接口：https://cloud.tencent.com/document/product/460/84797
     * 创建成功之后轮询任务，获取任务执行状态
     **/
    function createTtsTask(txtUrl, outputFile, callback) {
        cos.request({
            ...bucketConf,
            Method: 'POST',
            Url:  `https://${bucketConf.Bucket}.ci.${bucketConf.Region}.myqcloud.com/jobs`,
            Key: '/jobs', /** 固定值，必须 */
            ContentType: 'application/xml', /** 固定值，必须 */
            Body: COS.util.json2xml({
                Request: {
                    Tag: 'Tts',
                    Operation: {
                        TtsConfig: {
                            InputType: 'Url',       // 支持输入文本文件 Url 或 utf8-字符，utf8-字符长度不超过300，Url文件大小不超过10000个字符
                            Input: txtUrl        // 将步骤2中识别到的文本作为输入
                        },
                        TtsTpl: {
                            Mode: "Asyc",           // 支持同步和异步模式，如果文本内容长，建议选择异步模式
                            Codec: "mp3",           // 生成mp3音频，支持wav、mp3和pcm格式
                            VoiceType: "ruxue"      // 支持多种音色选择
                        },
                        Output: {
                            Bucket: bucketConf.Bucket,
                            Region: bucketConf.Region,
                            Object: outputFile      // 输出音频的地址
                        }
                    }
                }
            })            
        }, (err, data) => {
            if (err || !data?.Response?.JobsDetail?.JobId) {
                console.log(JSON.stringify(err));
                return callback && callback (err)
            }

            // 如果任务执行成功，则轮询任务执行结果
            const jobId = data?.Response?.JobsDetail?.JobId || "";
            jobId && queryTask(jobId, (queryErr) => {
                if(queryErr) {
                    return callback && callback(queryErr)
                }
                callback && callback(null, data)
            })
        })
    }

    /**
     * 轮询查询任务的执行情况，参考接口： https://cloud.tencent.com/document/product/460/84765
     * 也可以通过回调的方式获取任务执行结果：https://cloud.tencent.com/document/product/460/84958
     **/
    function queryTask(jobId, callback) {
        TaskInterval && clearInterval(TaskInterval);
        TaskInterval = setInterval(() => {
            cos.request({
                ...bucketConf,
                Method: 'GET',
                Url: `https://${bucketConf.Bucket}.ci.${bucketConf.Region}.myqcloud.com/jobs/${jobId}`,
                Key: `/jobs/${jobId}`          
            }, (err, data) => {
                if(err) {
                    console.log(JSON.stringify(err));
                    return callback && callback(err);
                }
                if (data?.Response?.JobsDetail?.State === 'Success') {
                    clearInterval(TaskInterval);
                    callback && callback();
                }
                if (data?.Response?.JobsDetail?.State === 'Failed') {
                    clearInterval(TaskInterval);
                    callback && callback ({
                        code: data?.Response?.JobsDetail?.Code,
                        message: data?.Response?.JobsDetail?.Message,
                    });
                }
            })
        }, 3000)
    }

  </script>
</body>
</html>
